Skip to content

fix(homelab): make memory and ZFS alerts durable - #2008

Open
shepherdjerred wants to merge 1 commit into
mainfrom
pagerduty-alerts-last-7d
Open

fix(homelab): make memory and ZFS alerts durable#2008
shepherdjerred wants to merge 1 commit into
mainfrom
pagerduty-alerts-last-7d

Conversation

@shepherdjerred

Copy link
Copy Markdown
Owner

Summary

  • Correct the 24-hour memory comparison so historical memory and ARC selectors are all offset together.
  • Discover ZFS collector pods and managed pools per node, route commands to the owning pod, and fail with actionable context.
  • Alert on never-scrubbed pools, remove obsolete DaemonSet RBAC, and document operator verification.

Verification

  • CDK8s focused tests, typecheck, lint, and manifest build pass.
  • Temporal focused tests and changed-file lint pass.
  • Temporal package-wide typecheck remains blocked by pre-existing missing Glitter workspace artifacts.

No live deployment, scrub, or PagerDuty mutation was performed.

@shepherdjerred

Copy link
Copy Markdown
Owner Author

This change is part of the following stack:

Change managed by git-spice.

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Aug 8, 2026

Copy link
Copy Markdown

Code Review by Qodo

Grey Divider

New Review Started

This review has been superseded by a new analysis

Grey Divider

Qodo Logo

@qodo-code-review

qodo-code-review Bot commented Aug 8, 2026

Copy link
Copy Markdown

PR Summary by Qodo

Fix memory offset logic and make ZFS maintenance pod-aware for durable alerts

🐞 Bug fix ✨ Enhancement 🧪 Tests 📝 Documentation ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

AI Description

• Fix MemoryLeakSuspected PromQL so all historical memory/ARC selectors are offset together.
• Discover per-node ZFS collector pods and managed pools; exec only on the owning pod.
• Alert on never-scrubbed pools, simplify RBAC, and document the verification workflow.
Diagram

graph TD
  SCH["Temporal schedule"] --> WRK["Temporal worker"] --> ZACT["ZFS maintenance"] --> K8S[["K8s pod list"]]
  ZACT --> KEX["kubectlExecInPod"] --> PODS(["ZFS collector pods"])
  PODS --> PROM["Prometheus"] --> RULES["Alert rules"] --> AM["Alertmanager/PagerDuty"]
  subgraph Legend
    direction LR
    _svc["Service"] ~~~ _api[["K8s API"]] ~~~ _pod(["Pod"])
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Use Kubernetes Exec API (WebSocket) instead of spawning kubectl
  • ➕ Avoids shelling out to kubectl (less dependency on image/tooling and PATH)
  • ➕ Potentially better structured error handling and streaming
  • ➕ Easier to unit-test by mocking client calls
  • @kubernetes/client-node exec is WebSocket-based and has known Bun compatibility issues (already documented in this repo)
  • ➖ Higher implementation complexity and more failure modes to debug in-cluster
2. Maintain a static per-node pool inventory (config) instead of discovery
  • ➕ Deterministic behavior; avoids parsing zpool list output
  • ➕ Can validate configuration at build-time
  • ➖ Inventory drifts when pools change; creates a new manual source of truth
  • ➖ Still requires correct per-node pod routing, so not eliminating the core problem

Recommendation: Keep the PR’s approach (discover one Running/Ready collector pod per node, then discover managed pools per node) because it matches heterogeneous node inventories and fails fast with actionable context. Spawning kubectl remains the pragmatic choice under Bun until the Kubernetes Exec client is reliable; the added pool-name validation and error wrapping meaningfully reduce the risk surface.

Files changed (13) +659 / -177

Enhancement (3) +295 / -51
zfs-maintenance.tsMake ZFS maintenance discover collector pods and pools per node +191/-51

Make ZFS maintenance discover collector pods and pools per node

• Reworks the activity to list collector pods by label, select one Running/Ready pod per node, and discover managed pools ('zfspv-pool-*') on each node. Routes all autotrim/scrub commands to the owning pod, adds pool-name validation, emits richer heartbeats, and wraps command failures with node/pod/pool/command context.

packages/temporal/src/activities/zfs-maintenance.ts

kubectl-exec.tsAdd shared kubectl exec utility with consistent errors +39/-0

Add shared kubectl exec utility with consistent errors

• Introduces 'kubectlExecInPod' to standardize 'kubectl exec' invocation for both string and argv commands. Provides a uniform error message including namespace/pod and command label.

packages/temporal/src/shared/kubectl-exec.ts

kubernetes-node-pods.tsAdd shared helper for selecting one Running/Ready pod per node +65/-0

Add shared helper for selecting one Running/Ready pod per node

• Introduces 'selectRunningReadyNodePods', a reusable selector that filters Running/Ready pods, enforces one pod per node, errors with actionable context, and returns results in stable node-sorted order.

packages/temporal/src/shared/kubernetes-node-pods.ts

Bug fix (2) +3 / -3
resource-monitoring.tsFix MemoryLeakSuspected PromQL offset application +1/-1

Fix MemoryLeakSuspected PromQL offset application

• Corrects the alert expression so the historical comparison offsets both memory selectors and ARC together. Prevents ARC cache drops from being misinterpreted as 24h non-ARC memory growth.

packages/homelab/src/cdk8s/src/resources/monitoring/monitoring/rules/resource-monitoring.ts

zfs-maintenance.tsAlert when scrub timestamp is zero (never scrubbed) or overdue +2/-2

Alert when scrub timestamp is zero (never scrubbed) or overdue

• Updates 'ZfsScrubOverdue' to include pools with 'zfs_zpool_last_scrub_completion_timestamp == 0', and updates the description to reflect the new intent. Makes never-scrubbed pools visible instead of silently excluded.

packages/homelab/src/cdk8s/src/resources/monitoring/monitoring/rules/zfs-maintenance.ts

Refactor (2) +24 / -114
bugsink.tsRefactor Bugsink housekeeping to shared kubectl exec helper +7/-34

Refactor Bugsink housekeeping to shared kubectl exec helper

• Replaces the local 'kubectl exec' implementation with 'kubectlExecInPod' for consistent spawning and error messages. Keeps the existing rationale for using kubectl under Bun.

packages/temporal/src/activities/bugsink.ts

velero-orphan-audit.tsReuse shared node-pod selection and kubectl exec helper +17/-80

Reuse shared node-pod selection and kubectl exec helper

• Replaces duplicated Running/Ready per-node pod selection logic with 'selectRunningReadyNodePods' and swaps the local exec implementation for 'kubectlExecInPod'. Reduces repeated parsing/error logic and standardizes failure messages.

packages/temporal/src/activities/velero-orphan-audit.ts

Tests (3) +262 / -0
resource-monitoring.test.tsAdd regression test for MemoryLeakSuspected offset semantics +23/-0

Add regression test for MemoryLeakSuspected offset semantics

• Adds a focused test asserting that the 24h 'offset' modifier is applied to total memory, available memory, and ARC size consistently in the generated PrometheusRule expression.

packages/homelab/src/cdk8s/src/resources/monitoring/monitoring/rules/resource-monitoring.test.ts

zfs-maintenance.test.tsAdd test ensuring never-scrubbed pools are alertable +22/-0

Add test ensuring never-scrubbed pools are alertable

• Adds a test asserting the 'ZfsScrubOverdue' alert fires when scrub completion timestamp is zero or overdue.

packages/homelab/src/cdk8s/src/resources/monitoring/monitoring/rules/zfs-maintenance.test.ts

zfs-maintenance.test.tsAdd comprehensive tests for ZFS maintenance discovery and routing +217/-0

Add comprehensive tests for ZFS maintenance discovery and routing

• Adds unit tests covering: selecting one Ready collector per node, handling duplicates/missing identity, parsing managed pools with validation, routing pools to the correct node’s pod, skipping in-progress scrubs, and surfacing command failures with context.

packages/temporal/src/activities/zfs-maintenance.test.ts

Documentation (2) +72 / -0
2026-04-04_homelab-audit-runbook.mdDocument per-node ZFS maintenance verification workflow +27/-0

Document per-node ZFS maintenance verification workflow

• Adds a ZFS maintenance runbook section describing how the weekly Temporal schedule discovers pools per node. Provides concrete commands to locate the correct collector pod and inspect pool status, and clarifies that a zero scrub timestamp is intentionally alertable.

packages/docs/guides/2026-04-04_homelab-audit-runbook.md

2026-08-08_memory-and-zfs-alert-remediation.mdAdd remediation plan for durable memory and ZFS alerts +45/-0

Add remediation plan for durable memory and ZFS alerts

• Introduces a plan document explaining the root causes of the false-positive memory incident and the ZFS maintenance failure mode. Captures the intended fixes, verification status, and post-rollout operator steps.

packages/docs/plans/2026-08-08_memory-and-zfs-alert-remediation.md

Other (1) +3 / -9
worker.tsTighten RBAC and remove DaemonSet access for ZFS maintenance +3/-9

Tighten RBAC and remove DaemonSet access for ZFS maintenance

• Updates RBAC commentary and removes now-obsolete 'daemonsets.apps get' permissions. The workflow now lists pods and execs directly into one Running/Ready pod per node.

packages/homelab/src/cdk8s/src/resources/temporal/worker.ts

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9228a348a0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

const NAMESPACE = "prometheus";
const TARGET = "daemonset/zfs-zpool-collector";
const POOLS = ["zfspv-pool-nvme", "zfspv-pool-hdd"] as const;
const ZFS_COLLECTOR_LABEL = "app=zfs-zpool-collector";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Add the selector label to the collector pod template

The new discovery query selects app=zfs-zpool-collector, but zfs-zpool.ts places that label only on the DaemonSet's metadata, not its podMetadata; Kubernetes does not propagate workload labels to pods, and the replaced code explicitly documented that these pods only have cdk8s-generated labels. Consequently the API returns no candidates and every weekly maintenance run fails before scrubbing any pool. Add the label to the pod template or select using a label the pods actually carry.

AGENTS.md reference: AGENTS.md:L66-L66

Useful? React with 👍 / 👎.

Comment on lines +35 to +36
if (isReady !== true || pod.status?.phase !== "Running") {
continue;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Fail when any collector node is not ready

When one node's collector is Pending, CrashLooping, or otherwise not Ready while another node's collector is healthy, this silently drops the unavailable node and returns a successful maintenance result for only the healthy node. That leaves the skipped node's pool unscrubbed and prevents Temporal's workflow-failure alerting from reporting the incomplete run; validate that every node represented by the collector candidates has a selected Ready pod and fail otherwise.

AGENTS.md reference: AGENTS.md:L83-L86

Useful? React with 👍 / 👎.

@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (1) 📜 Skill insights (0)

Grey Divider


Remediation recommended

1. PromQL string exceeds 120 chars 📘 Rule violation ⚙ Maintainability
Description
New PromQL expressions are embedded as single-line string literals that exceed the 120-character
maximum, which will violate the repo's line-length compliance and hinder readability/maintenance.
Code

packages/homelab/src/cdk8s/src/resources/monitoring/monitoring/rules/resource-monitoring.test.ts[R112-114]

+    expect(alert.expr.value).toBe(
+      "((node_memory_MemTotal_bytes - node_memory_MemAvailable_bytes) - on(instance) group_left node_zfs_arc_size) - ((node_memory_MemTotal_bytes offset 24h - node_memory_MemAvailable_bytes offset 24h) - on(instance) group_left node_zfs_arc_size offset 24h) > 8589934592",
+    );
Evidence
PR Compliance ID 2598417 requires every non-generated source line to be <= 120 characters. The added
expect(alert.expr.value).toBe(...) PromQL string and the updated PromQL string in
resource-monitoring.ts are single-line literals that clearly exceed this limit.

Rule 2598417: Enforce maximum line length of 120 characters
packages/homelab/src/cdk8s/src/resources/monitoring/monitoring/rules/resource-monitoring.test.ts[112-114]
packages/homelab/src/cdk8s/src/resources/monitoring/monitoring/rules/resource-monitoring.ts[93-93]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The PR adds lines that exceed the 120-character maximum due to long single-line PromQL string literals.

## Issue Context
This repo enforces a 120-character max line length for non-generated source files.

## Fix Focus Areas
- packages/homelab/src/cdk8s/src/resources/monitoring/monitoring/rules/resource-monitoring.test.ts[112-114]
- packages/homelab/src/cdk8s/src/resources/monitoring/monitoring/rules/resource-monitoring.ts[93-93]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

2. Scrub alert value zero 🐞 Bug ◔ Observability
Description
ZfsScrubOverdue now uses ts == 0 or (time() - ts) > 777600, which (for never-scrubbed pools)
yields a sample value of 0 due to or preferring the left-hand side. The alert still fires, but
the notification value becomes less useful (it no longer reflects elapsed time for the
never-scrubbed case).
Code

packages/homelab/src/cdk8s/src/resources/monitoring/monitoring/rules/zfs-maintenance.ts[R63-64]

+            "zfs_zpool_last_scrub_completion_timestamp == 0 or (time() - zfs_zpool_last_scrub_completion_timestamp) > 777600",
          ),
Evidence
The rule expression uses an or with a == 0 branch, while the collector script defines 0 as the
sentinel for never-scrubbed pools; this combination makes the alert’s sample value for that case
become 0 rather than an elapsed duration.

packages/homelab/src/cdk8s/src/resources/monitoring/monitoring/rules/zfs-maintenance.ts[52-66]
packages/homelab/src/cdk8s/src/resources/monitoring/scripts/zfs_zpool.sh[62-65]
packages/homelab/src/cdk8s/src/resources/monitoring/scripts/zfs_zpool.sh[86-103]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The `ZfsScrubOverdue` PromQL expression uses `A or B` where `A` is `zfs_zpool_last_scrub_completion_timestamp == 0`. For pools that have never scrubbed, this causes Prometheus to keep the left-hand sample value (`0`) instead of the elapsed-time value, reducing the usefulness of `$value` in alert notifications.

## Issue Context
The collector script explicitly emits `zfs_zpool_last_scrub_completion_timestamp` as `0` when no completed scrub is recorded.

## Fix Focus Areas
- packages/homelab/src/cdk8s/src/resources/monitoring/monitoring/rules/zfs-maintenance.ts[52-66]
- packages/homelab/src/cdk8s/src/resources/monitoring/monitoring/rules/zfs-maintenance.test.ts[18-20]

## Suggested fix
Prefer a single elapsed-time condition that already covers `ts == 0`, e.g.:
- `time() - zfs_zpool_last_scrub_completion_timestamp > 777600`

This keeps the alert firing for never-scrubbed pools (since `time() - 0` is large) while preserving an elapsed-time value for triage. Update the unit test expectation accordingly.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context used
✅ Compliance rules (platform): 66 rules

To customize comments, go to the Qodo configuration screen, or learn more in the docs.

Qodo Logo

Comment on lines +112 to +114
expect(alert.expr.value).toBe(
"((node_memory_MemTotal_bytes - node_memory_MemAvailable_bytes) - on(instance) group_left node_zfs_arc_size) - ((node_memory_MemTotal_bytes offset 24h - node_memory_MemAvailable_bytes offset 24h) - on(instance) group_left node_zfs_arc_size offset 24h) > 8589934592",
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

1. Promql string exceeds 120 chars 📘 Rule violation ⚙ Maintainability

New PromQL expressions are embedded as single-line string literals that exceed the 120-character
maximum, which will violate the repo's line-length compliance and hinder readability/maintenance.
Agent Prompt
## Issue description
The PR adds lines that exceed the 120-character maximum due to long single-line PromQL string literals.

## Issue Context
This repo enforces a 120-character max line length for non-generated source files.

## Fix Focus Areas
- packages/homelab/src/cdk8s/src/resources/monitoring/monitoring/rules/resource-monitoring.test.ts[112-114]
- packages/homelab/src/cdk8s/src/resources/monitoring/monitoring/rules/resource-monitoring.ts[93-93]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +63 to 64
"zfs_zpool_last_scrub_completion_timestamp == 0 or (time() - zfs_zpool_last_scrub_completion_timestamp) > 777600",
),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Informational

2. Scrub alert value zero 🐞 Bug ◔ Observability

ZfsScrubOverdue now uses ts == 0 or (time() - ts) > 777600, which (for never-scrubbed pools)
yields a sample value of 0 due to or preferring the left-hand side. The alert still fires, but
the notification value becomes less useful (it no longer reflects elapsed time for the
never-scrubbed case).
Agent Prompt
## Issue description
The `ZfsScrubOverdue` PromQL expression uses `A or B` where `A` is `zfs_zpool_last_scrub_completion_timestamp == 0`. For pools that have never scrubbed, this causes Prometheus to keep the left-hand sample value (`0`) instead of the elapsed-time value, reducing the usefulness of `$value` in alert notifications.

## Issue Context
The collector script explicitly emits `zfs_zpool_last_scrub_completion_timestamp` as `0` when no completed scrub is recorded.

## Fix Focus Areas
- packages/homelab/src/cdk8s/src/resources/monitoring/monitoring/rules/zfs-maintenance.ts[52-66]
- packages/homelab/src/cdk8s/src/resources/monitoring/monitoring/rules/zfs-maintenance.test.ts[18-20]

## Suggested fix
Prefer a single elapsed-time condition that already covers `ts == 0`, e.g.:
- `time() - zfs_zpool_last_scrub_completion_timestamp > 777600`

This keeps the alert firing for never-scrubbed pools (since `time() - 0` is large) while preserving an elapsed-time value for triage. Update the unit test expectation accordingly.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant